旧游无处不堪寻
无寻处,惟有少年心
C Primer Plus(一)

从本篇开始,我们就要重新学习 C 语言了,参考书籍为《C Primer Plus》,是个硬骨头,加油吧💪。

初识


起源

1972 年,贝尔实验室的 Dennis Ritch 和 Ken Thompson 在开发 Unix 系统时,设计了 C 语言,C 语言是在 B 语言的基础上进行设计的。

使用理由

虽然距离诞生已经过去 40 多年,而且在近 20 年中,有很多人转而使用脱胎于 C 语言的其他语言,如 C++、Objective-C、Java 等,但是 C 语言仍凭借自身的优点活跃在编程语言中。我们来看一下 C 语言的具体优点:

  • 设计特性: C 融合了计算机科学理论和实践的控制特性,可以让用户能轻松完成自顶向下的规划、结构化编程和模块化设计
  • 高效性: C 语言具有汇编语言才有的微调控能力,可以获得最大运行速度以及可以更有效地使用内存
  • 可移植性: 可移植性意味着,在一种系统编写的 C 程序稍作修改或不修改就能在其它系统运行。由于 C 语言与 Unix 关系密切,Unix 通常会将 C 编译器作为软件包的一部分。Linux 通常也会安装 C 编译器,运行各版本的 Windows 和 Mac 都能找到合适的 C 编译器。因此,无论使用何种计算机,都能找到针对特定系统的 C 编译器
  • 强大灵活
  • 面向程序员: C 是为了满足程序员的需求而设计的,利用 C 可以访问硬件、操控内存中的位。可以让程序员简洁地表达自己的意图

语言标准

K&R C

C 语言发展之初,并没有标准,1987 年,Brian Kernighan 和 Dennis Ritch 合著的 The C Programming Language 第一版是公认的 C 标准。称为 K&R C 或经典 C。

C89/C90

由于应用越来越广泛,需要一个更为严格的标准,ANSI 于 1989 年定义了 C 语言和 C 标准库,ISO 于 1990 年采用这一标准,这一版本通常称为 C89 或 C90,也因为 ANSI 先公布 C 标准,因此通常称为 ANSI C。

C99

1994 年,ANSI/ISO 联合委员会开始修订 C 标准,最终发布 C99,这一标准并未在 C 语言中添加新特性,而是把国际化、弥补缺陷和提高计算实用性作为主要修订目标,并尽量与 C90 兼容。

C11

2011 年,标准委员会发布了 C11 标准,强调跟进新技术。

编程机制

C 的基本策略是,将源文件转换为可执行文件。典型的 C 实现是通过编译和链接两个步骤来完成这一过程的。
编译器将源文件编译为中间代码或目标代码,链接器将中间代码与其他代码合并,生成可执行文件。C 使用这种方法便于模块化,可以独立编译单独的模块,再用链接器合并已经编译好的模块,如果只更改某一模块,我们可以不重新编译其他模块。链接器还可以将自己编写的程序与库函数链接在一起,对于库代码,链接器只会将程序用到的库函数提取出来。

中间代码或目标代码也是由机器指令组成,只不过目标文件只包含编译器将我们编写的代码编译出的机器指令,可执行文件还包含程序中使用的库函数和启动代码。

有些系统中必须分别编译和链接程序,而在另外的系统中编译器会自动启动链接器,用户只需给出编译命令即可。

GNU Compiler Collection 和 LLVM

GNU Compiler Collection 包含 GCC C Compiler,GCC 在持续不断地开发,紧跟 C 标准改动,GCC 有各种版本以适应不同的平台和操作系统,使用 gcc 命令便可调用 GCC C Compiler,并且许多系统使用 cc 作为 gcc 别名。gcc 基本用法为:

gcc [option] [file]

LLVM 是 gcc 的另一个替代品,该项目是与编译器相关的开源软件集合,他的 Clang 编译器前端处理 C 代码,可通过 clang 命令调用,有多个不同版本支持不同平台。

各系统都使用 cc 别名来替代 gcc 或 clang。
我们可以使用如下命令显示版本信息:

cc -v

C 语言概述


语句

C 语言标准中有 6 种语句:

  • 标号语句
  • 复合语句
  • 表达式语句
  • 选择语句
  • 迭代语句
  • 跳转语句

注意: C 语言是通过赋值运算符而不是赋值表达式完成赋值操作的,C 标准中并没有赋值语句,平常我们提到的赋值语句其实是表达式语句。C 标准中也没有函数调用语句,本质也是表达式语句。

提高程序可读性

  • 选择有意义的函数名
  • 写注释
  • 函数中用空行分隔多个部分
  • 每条语句各占一行

函数

C 90 标准新增了函数原型(prototype),函数原型是一种声明形式,因此函数原型也被称为函数声明(function declaration)。语法如下:

void butler(void);

之后,我们还需要函数定义,即函数的真正实现:

void butler(void) {
printf("yes sir");
}

C 标准建议,为程序中用到的函数都提供函数声明。

数据和 C


数据类型关键字

C 语言基本类型关键字,K&R C 有 7 个类型相关的关键字。C90 添加了 2 个关键字,C99 又添加了 3 个关键字:

K&R C C90 C99
int signed _Bool
long void _Complex
short _Imaginary
unsigned
char
float
double

整数类型

注意: C 语言只规定 short 占用的内存空间不能多于 int,long 占用的内存空间不能少于 int。
现在个人计算机常见配置,long long 占 64 位,long 占 32 位,short 占 16 位,int 占 16 或 32 位。原则上四种类型代表四种不同大小,实际使用中会有重叠。

通常,程序中的数字都被存储为 int,如果存储不下,编译器会将其视为 long,如果还超过 long 的范围,编译器会将其视为 unsigned long,如果还不够,则编译器视为 long long。
有些情况下,需要使用 long 类型存储一个小数字,我们要在数值结尾添加一个 l 或 L。类似的,在支持 long long 类型的系统,我们在字面量后附加 ll 或 LL 来表示 long long 类型。另外,u 或 U 表示 unsigned。

注意: 如果存储的变量超出范围,则会从起点重新开始,溢出行为是未定义行为,即 C 标准并未定义溢出规则。

char 类型

char 类型用于存储字符,但是从技术层面看,char 本质还是整数类型,计算机采用数字编码来处理字符,即用特定的整数表示特定的字符。之后我可能会讲一些关于字符编码的知识,目前常用的字符编码为 ASCII 和 Unicode 编码,Unicode 兼容 ASCII。标准 ASCII 编码为 0~127,只需要 7 位二进制数即可,许多系统还提供 ASCII 扩展编码,使用了 8 位二进制。

C 语言把 1 个字节定义为 char 类型占用的位数。用单引号括起来的单个字符称为字符常量:

char grade = 'A';

实际上,字符是以整数进行存储的,因此,我们也可以直接将整数赋值给字符变量:

char grade = 65;

但是这不是一种好的编程风格。

C 语言提供两种方式表示控制字符:

  1. 使用 ASCII 码
  2. 使用转义字符

注意: \n 代表换行,将光标移动到下一行的起始位置。\r 代表回车,将光标移动到当前行的起始位置。\0oo 和 \xhh 是 ASCII 码的特殊表示,如果用八进制表示一个字符,例如编译器不识别 \a,我们可以使用 ASCII 码代替:

char beep = '\007'

此外,要注意,有些编译器把 char 类型实现为有符号类型,范围是 -128 ~ 127 之间,而有的编译器则实现为无符号类型,范围是 0 ~ 255。我们可以查阅 limits.h 文件确定。

根据 C90 标准,我们可以在 char 前使用 signed 或 unsigned 来限定到底是用什么类型,而不用管编译器的默认实现。

_Bool 类型

C99 新增了 _Bool 类型用于表示布尔值,实际也是整数类型,但原则上只占用一位存储空间。

浮点数类型

C 语言中浮点数包括:

  • float
  • double
  • long double

C 语言规定: float 类型必须至少保留 6 位有效数字,取值范围至少是 10-37~1037,double 类型和 float 类型表示的最小取值范围相同,但必须至少保留 10 位有效数字,对于 long double,C 语言只保证精度至少和 double 相同。

默认情况编译器假定浮点常量为 double 类型,如果想使用 float 类型,需要在常量后加 f 或 F。

printf()

printf() 函数把输出会先发送到缓冲区,然后再将缓冲区的内容发送到屏幕上,从缓冲区发送到屏幕或文件这一过程称为刷新缓冲区。C 标准明确规定如下三种情况会将内容发送到屏幕:

  • 缓冲区满
  • 遇到换行符
  • 需要输入,即遇到 scanf() 函数
  • 使用 fflush() 函数强制刷新缓冲区